package qmlt.learning.neuralnetwork;

import java.util.ArrayList;
import java.util.List;

import qmlt.dataset.Attribute;
import qmlt.dataset.DataSet;
import qmlt.dataset.Instance;
import qmlt.dataset.utils.ListUtils;
import qmlt.learning.Learner;
import qmlt.learning.neuralnetwork.control.NeuralNetworkController;
import qmlt.learning.neuralnetwork.node.InputNode;
import qmlt.learning.neuralnetwork.node.Node;
import qmlt.learning.neuralnetwork.node.OutputNode;
import qmlt.learning.neuralnetwork.node.OutputNodeC;

public class NeuralNetwork implements Learner<NeuralNetworkController>
{
	private NeuralNetworkController	controller;

	private List<Node>										nodes		= new ArrayList<Node>();

	private List<InputNode>								inodes	= new ArrayList<InputNode>();

	private List<List<Node>>							hlayers	= new ArrayList<List<Node>>();

	private OutputNode										onode;

	public void init() throws InstantiationException, IllegalAccessException
	{
		for (int i = 0; i < controller.getnInputs(); ++i)
		{
			InputNode inode = new InputNode("in" + i);
			inodes.add(inode);
			nodes.add(inode);
		}

		for (int k = 0; k < controller.getnHiddenLayerNodes().length; ++k)
		{
			List<Node> hnodes = new ArrayList<Node>();
			for (int i = 0; i < controller.getnHiddenLayerNodes()[k]; ++i)
			{
				Node hnode = new Node("hid" + k + "-" + i);
				hnodes.add(hnode);
				nodes.add(hnode);
			}
			hlayers.add(hnodes);

			if (k == 0)
			{
				for (InputNode inode : inodes)
				{
					for (Node node : hnodes)
					{
						inode.connectTo(node);
					}
				}
			}
			else
			{
				List<Node> prelayer = hlayers.get(k - 1);
				for (Node pnode : prelayer)
				{
					for (Node node : hnodes)
					{
						pnode.connectTo(node);
					}
				}
			}
		}

		onode = controller.getOutputNodeClass().newInstance();
		onode.setId("out");
		nodes.add(onode);

		List<? extends Node> lastlayer = (hlayers.size() > 0) ? hlayers.get(hlayers.size() - 1)
				: inodes;
		for (Node node : lastlayer)
		{
			node.connectTo(onode);
		}
	}

	@Override
	public void train(DataSet trainSet, NeuralNetworkController controller)
	{
		for (Attribute a : trainSet.getFeatureDefs())
		{
			assert a.type.equals(Attribute.FLOAT) : "neural networks apply to numeric features only.";
		}
		assert trainSet.getTargetDef().type.equals(Attribute.FLOAT) : "neural networks apply to regression problems only.";

		this.controller = controller;
		try
		{
			init();
		}
		catch (Exception e)
		{
			System.err.println("can't init ANN: " + e.getMessage());
			e.printStackTrace();
			return;
		}

		int size = trainSet.getInstances().size();

		int iter = 1;
		while (!controller.ifStop(this, iter))
		{
			int i = (int) (Math.random() * size);
			Instance inst = trainSet.getInstances().get(i);

			feedAnInstance(inst);

			for (int k = nodes.size() - 1; k >= 0; --k)
			{
				nodes.get(k).backPropagate();
			}
			for (int k = nodes.size() - 1; k >= 0; --k)
			{
				nodes.get(k).updateWeights(controller.getLearningRate(iter), controller.getMomentum(),
						controller.getDecay());
			}

			iter++;
		}
	}

	public void feedAnInstance(Instance inst)
	{
		List<Float> inputs = ListUtils.convert(inst.getFeatures());
		Float target = (Float) inst.getTarget();

		for (int j = 0; j < inputs.size(); ++j)
		{
			inodes.get(j).input(inputs.get(j));
		}
		onode.target = target;

		for (int k = 0; k < nodes.size(); ++k)
		{
			nodes.get(k).feedForward();
		}
	}

	@Override
	public Object predict(Instance instance)
	{
		feedAnInstance(instance);

		Float output = onode.getOutput();

		if (this.controller.getOutputNodeClass().equals(OutputNodeC.class))
		{
			return controller.convertOutputForClassification(output);
		}

		return output;
	}

	public void debugPrintHLayerWeights(int ilayer)
	{
		List<Node> layer = hlayers.get(ilayer);
		for (Node node : layer)
		{
			System.out.println(node.weights);
		}
	}

	@Override
	public void loadModel(String inputFilepath)
	{
		// TODO Auto-generated method stub

	}

	@Override
	public void saveModel(String outputFilepath)
	{
		// TODO Auto-generated method stub

	}
}
